namespace Angelus.Data.Linq.OracleClient
{
	using System;
	using System.Data.DLinq;
	using System.Data.DLinq.ProviderBase;
	using System.Data.OracleClient;
	using System.Text;

    /// <summary>
    /// 
    /// </summary>
    internal class OracleProviderType : ProviderType, IComparable<OracleProviderType>, IComparable
    {
        #region Nested TYpes

        /// <summary>
        /// 
        /// </summary>
        private enum TypeCategory
        {
            /// <summary>
            /// 
            /// </summary>
            Numeric,
            /// <summary>
            /// 
            /// </summary>
            Char,
            /// <summary>
            /// 
            /// </summary>
            Text,
            /// <summary>
            /// 
            /// </summary>
            Binary,
            /// <summary>
            /// 
            /// </summary>
            Image,
            /// <summary>
            /// 
            /// </summary>
            Xml,
            /// <summary>
            /// 
            /// </summary>
            DateTime,
            /// <summary>
            /// 
            /// </summary>
            UniqueIdentifier,
            /// <summary>
            /// 
            /// </summary>
            Variant,
            /// <summary>
            /// 
            /// </summary>
            Udt
        }

        #endregion

        #region Fields

        private const int _defaultDecimalPrecision = 0x1d;
        private const int _defaultDecimalScale = 4;
        private readonly OracleType _oracleType;
        private readonly int _precision;
        private readonly Type _runtimeOnlyType;
        private readonly int _scale;
        private readonly int _size;
        private decimal? _applicationTypeIndex;

        #endregion

        #region Constructors

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        private OracleProviderType(OracleType type)
        {
            this._applicationTypeIndex = new decimal?();
            this._oracleType = type;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="applicationTypeIndex"></param>
        private OracleProviderType(decimal applicationTypeIndex)
        {
            this._applicationTypeIndex = applicationTypeIndex;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        private OracleProviderType(Type type)
        {
            this._applicationTypeIndex = new decimal?();
            this._runtimeOnlyType = type;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <param name="size"></param>
        private OracleProviderType(OracleType type, int size)
        {
            this._applicationTypeIndex = new decimal?();
            this._oracleType = type;
            this._size = size;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <param name="precision"></param>
        /// <param name="scale"></param>
        private OracleProviderType(OracleType type, int precision, int scale)
        {
            this._applicationTypeIndex = new decimal?();
            this._oracleType = type;
            this._precision = precision;
            this._scale = scale;
        }

        #endregion

        #region Properties

        /// <summary>
        /// 
        /// </summary>
        private bool IsApplicationType
        {
            get { return this._applicationTypeIndex.HasValue; }
        }

        /// <summary>
        /// 
        /// </summary>
        internal override bool IsRuntimeOnlyType
        {
            get { return (null != this._runtimeOnlyType); }
        }

        /// <summary>
        /// 
        /// </summary>
        internal int Size
        {
            get { return this._size; }
        }

        /// <summary>
        /// 
        /// </summary>
        internal OracleType OracleType
        {
            get { return this._oracleType; }
        }

        #endregion

        #region Factory Implementation

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        internal static OracleProviderType Create(OracleType type)
        {
            return new OracleProviderType(type);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <param name="size"></param>
        /// <returns></returns>
        internal static OracleProviderType Create(OracleType type, int size)
        {
            if (0 == size)
            {
                return new OracleProviderType(type);
            }
            return new OracleProviderType(type, size);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <param name="precision"></param>
        /// <param name="scale"></param>
        /// <returns></returns>
        internal static OracleProviderType Create(OracleType type, int precision, int scale)
        {
            return new OracleProviderType(type, precision, scale);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="applicationTypeIndex"></param>
        /// <returns></returns>
        internal static OracleProviderType CreateApplicationType(int applicationTypeIndex)
        {
            return new OracleProviderType(applicationTypeIndex);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        internal static OracleProviderType CreateUDT(Type type)
        {
            return new OracleProviderType(type);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        internal static OracleProviderType From(Type type)
        {
            if (type.IsGenericType && (typeof (Nullable<>) == type.GetGenericTypeDefinition()))
            {
                type = type.GetGenericArguments()[0];
            }
            switch (Type.GetTypeCode(type))
            {
                case TypeCode.Object:
                    if (typeof (Guid) == type)
                    {
                        return Create(OracleType.Raw, 16);
                    }
                    if ((typeof (byte[]) == type) || (typeof (Binary) == type))
                    {
                        return Create(OracleType.Clob);
                    }
                    if ((typeof (char[]) == type))
                    {
                        return Create(OracleType.VarChar, 0xfa0);
                    }
                    if (typeof (TimeSpan) == type)
                    {
                        return Create(OracleType.TimestampWithTZ);
                    }
                    return CreateUDT(type);

                case TypeCode.Boolean:
                    return Create(OracleType.Number, 1);

                case TypeCode.Char:
                    return Create(OracleType.NChar, 1);

                case TypeCode.SByte:
                    return Create(OracleType.SByte);

                case TypeCode.Byte:
                    return Create(OracleType.Byte);

                case TypeCode.UInt16:
                    return Create(OracleType.UInt16);

                case TypeCode.Int16:
                    return Create(OracleType.Int16);

                case TypeCode.Int32:
                    return Create(OracleType.Int32);

                case TypeCode.UInt32:
                    return Create(OracleType.UInt32);

                case TypeCode.Int64:
                case TypeCode.Decimal:
                    return Create(OracleType.Number);

                case TypeCode.Single:
                    return Create(OracleType.Float);

                case TypeCode.Double:
                    return Create(OracleType.Double);

                case TypeCode.DateTime:
                    return Create(OracleType.DateTime);

                case TypeCode.String:
                    return Create(OracleType.VarChar, 0xfa0);
            }
            throw new NotImplementedException();
        }

        #endregion

        #region Provider Type Implementation

        #region IComparable Members

        /// <summary>
        /// 
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        int IComparable.CompareTo(object obj)
        {
            OracleProviderType type = (OracleProviderType) obj;
            return Compare(this, type);
        }

        #endregion

        #region IComparable<OracleProviderType> Members

        /// <summary>
        /// 
        /// </summary>
        /// <param name="that"></param>
        /// <returns></returns>
        public int CompareTo(OracleProviderType that)
        {
            return Compare(this, that);
        }

        #endregion

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        internal override int ComparePrecedenceTo(ProviderType type)
        {
            OracleProviderType provider = (OracleProviderType) type;
            int first = !this.IsApplicationType ? GetTypeCoercionPrecedence(this.OracleType) : 0x63;
            int second = !this.IsApplicationType ? GetTypeCoercionPrecedence(provider.OracleType) : 0x63;
            return first.CompareTo(second);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            OracleProviderType provider = (OracleProviderType) obj;
            return (0 == Compare(this, provider));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public static bool operator ==(OracleProviderType x, OracleProviderType y)
        {
            return (0 == Compare(x, y));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public static bool operator !=(OracleProviderType x, OracleProviderType y)
        {
            return (0 != Compare(x, y));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        internal override Type GetClosestRuntimeType()
        {
            if (null != this._runtimeOnlyType)
            {
                return this._runtimeOnlyType;
            }
            return GetClosestRuntimeType(this._oracleType);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="oracleType"></param>
        /// <returns></returns>
        internal static Type GetClosestRuntimeType(OracleType oracleType)
        {
            //switch (oracleType)
            //{
            //    case OracleType.Byte:
            //        return typeof(bool);

            //    case OracleType.Char:
            //    case OracleType.LongVarChar:
            //    case OracleType.NChar:
            //    case OracleType.NClob:
            //    case OracleType.NVarChar:
            //    case OracleType.VarChar:
            //    case OracleType.RowId:
            //        return typeof(string);

            //    case OracleType.Blob:
            //    case OracleType.Clob:
            //    case OracleType.Raw:
            //        return typeof(byte[]);

            //    case OracleType.Number:
            //        return typeof(decimal);

            //    case OracleType.DateTime:
            //    case OracleType.Timestamp:
            //    case OracleType.TimestampLocal:
            //    case OracleType.TimestampWithTZ:
            //        return typeof(DateTime);

            //    case OracleType.IntervalDayToSecond:
            //    case OracleType.IntervalYearToMonth:
            //        return typeof(TimeSpan);

            //    case OracleType.Double:
            //        return typeof(double);

            //    case OracleType.Float:
            //        return typeof(decimal);

            //    case OracleType.Int16:
            //        return typeof(Int16);

            //    case OracleType.Int32:
            //        return typeof(int);

            //    case OracleType.UInt16:
            //        return typeof(UInt16);

            //    case OracleType.UInt32:
            //        return typeof(UInt32);


            //    default:
            //        throw new NotImplementedException();
            //}


            switch (oracleType)
            {
                case OracleType.Byte:
                    return typeof (byte);

                case OracleType.Char:
                case OracleType.Clob:
                case OracleType.LongVarChar:
                case OracleType.NChar:
                case OracleType.NClob:
                case OracleType.NVarChar:
                case OracleType.RowId:
                case OracleType.VarChar:
                    return typeof (string);

                case OracleType.BFile:
                case OracleType.Blob:
                case OracleType.LongRaw:
                case OracleType.Raw:
                    return typeof (byte[]);

                case OracleType.DateTime:
                case OracleType.Timestamp:
                case OracleType.TimestampLocal:
                case OracleType.TimestampWithTZ:
                    return typeof (DateTime);

                case OracleType.IntervalDayToSecond:
                    return typeof (TimeSpan);

                case OracleType.Double:
                    return typeof (double);

                case OracleType.Number:
                    return typeof (decimal);

                case OracleType.Float:
                    return typeof (Single);

                case OracleType.Int16:
                    return typeof (Int16);

                case OracleType.IntervalYearToMonth:
                case OracleType.Int32:
                    return typeof (int);

                case OracleType.UInt16:
                    return typeof (UInt16);

                case OracleType.UInt32:
                    return typeof (UInt32);


                default:
                    throw new NotImplementedException();
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            int hashCode = 0;
            if (null != this._runtimeOnlyType)
            {
                hashCode = this._runtimeOnlyType.GetHashCode();
            }
            return ((((hashCode ^ this.OracleType.GetHashCode()) ^ this._size) ^ this._precision) ^ (this._scale << 8));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="oracleType"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        internal static object GetParameterData(OracleType oracleType, object value)
        {
            if (null == value)
            {
                return DBNull.Value;
            }
            Type valueType = value.GetType();
            Type providerType = GetClosestRuntimeType(oracleType);
            if (valueType == providerType)
            {
                return value;
            }
            return DBConvert.ChangeType(value, providerType);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        internal override bool IsApplicationTypeOf(int index)
        {
            if (this.IsApplicationType)
            {
                decimal? nullable = this._applicationTypeIndex;
                if (index == nullable.GetValueOrDefault())
                {
                    return nullable.HasValue;
                }
            }
            return false;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        internal override bool IsSameTypeFamily(ProviderType type)
        {
            OracleProviderType target = (OracleProviderType) type;
            if (this.IsApplicationType)
            {
                return false;
            }
            if (target.IsApplicationType)
            {
                return false;
            }
            TypeCategory category = GetCategory(this);
            TypeCategory targetCategory = GetCategory(target);
            return (category == targetCategory);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="stype"></param>
        /// <returns></returns>
        internal static OracleProviderType Parse(string stype)
        {
            int parenthesis = stype.IndexOf('(');
            int blank = stype.IndexOf(' ');
            int index = ((-1 != parenthesis) && (-1 != blank))
                            ? Math.Min(blank, parenthesis)
                            : ((-1 != parenthesis) ? parenthesis : ((-1 != blank) ? blank : -1));

            string typeName;
            string typeLength = null;
            string typeScale = null;

            if (-1 == index)
            {
                typeName = stype;
                index = stype.Length;
            }
            else
            {
                typeName = stype.Substring(0, index);
            }

            int startIndex = index;
            if ((startIndex < stype.Length) && (stype[index] == '('))
            {
                startIndex++;
                index = stype.IndexOf(',', startIndex);
                if (index > 0)
                {
                    typeLength = stype.Substring(startIndex, index - startIndex);
                    startIndex = index + 1;
                    index = stype.IndexOf(')', startIndex);
                    typeScale = stype.Substring(startIndex, index - startIndex);
                }
                else
                {
                    index = stype.IndexOf(')', startIndex);
                    typeLength = stype.Substring(startIndex, index - startIndex);
                }
                startIndex = index++;
            }

            if (0 == string.Compare(typeName, "rowid", StringComparison.OrdinalIgnoreCase))
            {
                typeName = "raw";
            }
            if (0 == string.Compare(typeName, "varchar2", StringComparison.OrdinalIgnoreCase))
            {
                typeName = "varchar";
            }
            if (0 == string.Compare(typeName, "date", StringComparison.OrdinalIgnoreCase))
            {
                typeName = "datetime";
            }
            if (0 == string.Compare(typeName, "cursor", StringComparison.OrdinalIgnoreCase))
            {
                typeName = "cursor";
            }

            OracleType oracleType = (OracleType) Enum.Parse(typeof (OracleType), typeName, true);
            int length = (null != typeLength) ? int.Parse(typeLength) : 0;
            int scale = (null != typeScale) ? int.Parse(typeScale) : 0;

            switch (oracleType)
            {
                case OracleType.Char:
                case OracleType.LongVarChar:
                case OracleType.NChar:
                case OracleType.NVarChar:
                case OracleType.VarChar:
                    return Create(oracleType, length);

                case OracleType.Double:
                case OracleType.Float:
                    return Create(oracleType, length, scale);

                default:
                    return Create(oracleType);
            }
        }

        /// <summary>
        ///
        /// </summary>
        /// <returns></returns>
        internal override bool SupportsComparison()
        {
            // TODO
            OracleType oracleType = this._oracleType;
            return true;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        internal override string ToQueryString()
        {
            return this.ToQueryString(QueryFormatOptions.None);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="formatFlags"></param>
        /// <returns></returns>
        internal override string ToQueryString(QueryFormatOptions formatFlags)
        {
            if (this._runtimeOnlyType != null)
            {
                return this._runtimeOnlyType.ToString();
            }
            StringBuilder builder2 = new StringBuilder();
            switch (this._oracleType)
            {
                case OracleType.DateTime:
                case OracleType.Int16:
                case OracleType.Int32:
                case OracleType.Number:
                    builder2.Append(this._oracleType.ToString());
                    break;


                case OracleType.Double:
                case OracleType.Float:
                    builder2.Append(this._oracleType);
                    if (this._precision != 0)
                    {
                        builder2.Append("(");
                        builder2.Append(this._precision);
                        if (this._scale != 0)
                        {
                            builder2.Append(",");
                            builder2.Append(this._scale);
                        }
                        builder2.Append(")");
                    }
                    break;

                case OracleType.Char:
                case OracleType.LongVarChar:
                case OracleType.NChar:
                case OracleType.NVarChar:
                case OracleType.VarChar:
                    builder2.Append(this._oracleType);
                    if ((this._size > 0) && ((formatFlags & QueryFormatOptions.SuppressSize) == QueryFormatOptions.None))
                    {
                        builder2.Append("(");
                        builder2.Append(this._size);
                        builder2.Append(")");
                    }
                    break;
            }
            return builder2.ToString();
        }

        #endregion

        #region Private Implementation

        /// <summary>
        /// 
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        private static int Compare(OracleProviderType x, OracleProviderType y)
        {
            if (x == y)
            {
                return 0;
            }
            if (null == x)
            {
                return -1;
            }
            if (null == y)
            {
                return 1;
            }
            int result = x.OracleType.CompareTo(y.OracleType);
            if (0 != result)
            {
                return result;
            }
            result = x._size.CompareTo(y._size);
            if (0 != result)
            {
                return result;
            }
            result = x._precision.CompareTo(y._precision);
            if (0 != result)
            {
                return result;
            }
            return x._scale.CompareTo(y._scale);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static int GetTypeCoercionPrecedence(OracleType type)
        {
            switch (type)
            {
                case OracleType.Byte:
                    return 0x10;
                case OracleType.Char:
                    return 1;
                case OracleType.DateTime:
                    return 0x16;
                case OracleType.Double:
                    return 0x12;
                case OracleType.Float:
                    return 20;
                case OracleType.Int16:
                    return 12;
                case OracleType.Int32:
                    return 13;
                case OracleType.IntervalDayToSecond:
                    return 0x20;
                case OracleType.IntervalYearToMonth:
                    return 0x21;
                case OracleType.LongRaw:
                    return 0x30;
                case OracleType.LongVarChar:
                    return 0x31;
                case OracleType.NChar:
                    return 2;
                case OracleType.Number:
                    return 0x12;
                case OracleType.NVarChar:
                    return 3;
                case OracleType.Raw:
                    return 10;
                case OracleType.RowId:
                    return 0x32;
                case OracleType.SByte:
                    return 11;
                case OracleType.Timestamp:
                    return 0x40;
                case OracleType.TimestampLocal:
                    return 0x41;
                case OracleType.TimestampWithTZ:
                    return 0x42;
                case OracleType.UInt16:
                    return 14;
                case OracleType.UInt32:
                    return 15;
                case OracleType.VarChar:
                    return 0;
                default:
                    throw new Exception("Unhandled type: " + type);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static TypeCategory GetCategory(OracleProviderType type)
        {
            switch (type.OracleType)
            {
                case OracleType.Byte:
                case OracleType.Double:
                case OracleType.Float:
                case OracleType.Int16:
                case OracleType.Int32:
                case OracleType.Number:
                case OracleType.SByte:
                case OracleType.UInt16:
                case OracleType.UInt32:
                    return TypeCategory.Numeric;

                case OracleType.DateTime:
                case OracleType.Timestamp:
                case OracleType.TimestampLocal:
                case OracleType.TimestampWithTZ:
                    return TypeCategory.DateTime;

                case OracleType.Char:
                case OracleType.LongVarChar:
                case OracleType.NChar:
                case OracleType.NClob:
                case OracleType.NVarChar:
                case OracleType.VarChar:
                case OracleType.RowId:
                    return TypeCategory.Char;

                case OracleType.IntervalDayToSecond:
                case OracleType.IntervalYearToMonth:
                    return TypeCategory.DateTime;

                case OracleType.Blob:
                case OracleType.Clob:
                case OracleType.Raw:
                    return TypeCategory.Binary;

                default:
                    throw new ArgumentException("Unhandled type:" + type);
            }
        }

        #endregion
    }
}